/*
 * This software is released under the BSD license. Full license available at http://xmappr.googlecode.com
 *
 * Copyright (c) 2008, 2009, Peter Knego & Xmappr contributors
 * All rights reserved.
 */
package org.xmappr;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.nio.charset.Charset;
import java.util.*;

/**
 * A wrapper around {@link javax.xml.stream.XMLStreamReader} that enables access to XML on the element level.
 * It's no longer necessary to create a loop pulling and processing events. A simple usage example:<br><br>
 * {@code XMLSimpleReader reader = new XMLSimpleReader(xmlStreamReader);}<br>
 * {@code reader.getNodeName();}<br>
 * {@code reader.getAttributes();}
 *
 * @author peter
 */
public class XMLSimpleReader {

    private XMLStreamReader reader;
    private XmlStreamSettings settings;

    private Stack<Element> elementStack = new Stack<Element>();
    private boolean isEnd = false;
    private boolean isStoringUnknownElements;
    private ObjectStore eventCache;
    private ObjectStore objectStore;
    private QName rootName;

    public XMLSimpleReader(XMLStreamReader reader, XmlStreamSettings settings) {
        this(reader, false, settings);
    }

    public XMLSimpleReader(XMLStreamReader reader, boolean isStoringUnknownElements, XmlStreamSettings settings) {
        this.reader = reader;
        this.isStoringUnknownElements = isStoringUnknownElements;
        this.settings = settings;
        if (this.isStoringUnknownElements) {
            objectStore = new ObjectStore(200, 200);
            eventCache = new ObjectStore(200, 200);
        }
    }

    public ObjectStore getObjectStore() {
        return objectStore;
    }

    private int nextEvent() {
        try {
            return reader.next();
        } catch (XMLStreamException e) {
            throw new XmapprException("Error reading XML stream.", e);
        }
    }

    private boolean nextElementBoundary(boolean processEvents) {

        // checkAndReset the accumulated Text
        if (!elementStack.isEmpty()) {
            elementStack.peek().resetText();
        }
        // read stream settings at the beginning of the document
        if (reader.getEventType() == XMLStreamConstants.START_DOCUMENT) {
            // nothing to do - encoding is set by the external Reader
        }

        for (int event = nextEvent(); true; event = nextEvent()) {
            switch (event) {
                case XMLStreamConstants.START_ELEMENT:
                    if (isStoringUnknownElements) {
                        eventCache.trim();
                        eventCache.mark();
                        storeElement(eventCache);
                    }
                    return true;
                case XMLStreamConstants.END_DOCUMENT:
                    if (isStoringUnknownElements) {
                        storeElement(eventCache);
                    }
                    isEnd = true;
                    return false;
                case XMLStreamConstants.END_ELEMENT:
                    if (isStoringUnknownElements) {
                        eventCache.trim();
                        storeElement(eventCache);
                    }
                    return false;
                case XMLStreamConstants.CHARACTERS:
                    if (processEvents && !reader.isWhiteSpace()) {
                        String text = reader.getText();
//                        System.out.println("TEXT" + elementStack.peek().hashCode() + ":" + text + "|");
                        if (isStoringUnknownElements) {
                            storeElement(eventCache);
                        }
                        elementStack.peek().addText(text);
                    }
                    break;
            }
        }
    }

    /**
     * Checks if a next child node exists and moves into it.<br><br>
     * Postions the underlying xml stream to the opening element of the child node.
     *
     * @return True if next child node exists, otherwise false.
     */
    public boolean moveDown() {

        int event = reader.getEventType();
        if (event == XMLStreamConstants.START_ELEMENT) {
            Element element = new Element();
            element.name = reader.getName();
            int attrCount = reader.getAttributeCount();
            for (int i = 0; i < attrCount; i++) {
                element.putAttribute(reader.getAttributeName(i), reader.getAttributeValue(i));
            }
            elementStack.push(element);
        } else {
            if (event != XMLStreamConstants.END_ELEMENT && event != XMLStreamConstants.END_DOCUMENT) {
                throw new XmapprException("ERROR: this should be a node END. Instead it's a event=" + event);
            }
            return false;
        }
        nextElementBoundary(true);
        return true;
    }

    /**
     * Moves back from a child node into the parent node.
     * Postions the underlying xml stream to the closing element of the child node.
     */
    public void moveUp() {
        if (reader.getEventType() == XMLStreamConstants.END_ELEMENT) {
            elementStack.pop();
            nextElementBoundary(true);
            return;
        }
        int depth = 1;
        boolean continueLooping = true;
        while (continueLooping) {
            if (nextElementBoundary(true)) {  // node START
                depth++;
            } else {      // node END
                if (depth-- == 0) {
                    continueLooping = false;
                    nextElementBoundary(true);
                }
            }
        }
        elementStack.pop();
    }

    public String getText() {
        StringBuilder text = elementStack.peek().text;
        if (text.length() == 0) return "";
        return text.toString();
    }

    public QName getName() {
        if (elementStack.isEmpty()) {
            return null;
        }
        return elementStack.peek().name;
    }

    public String getAttribute(QName attributeName) {
        return elementStack.peek().getAttribute(attributeName);
    }

    public Iterator<Map.Entry<QName, String>> getAttributeIterator() {
        return elementStack.peek().iterator();
    }

    public static class Element implements Iterable {
        public QName name;
        private StringBuilder text;
        private Map<QName, String> attributes = new HashMap<QName, String>();

        public void addText(String text) {
            this.text.append(text);
        }

        public void resetText() {
            if (text == null) {
                text = new StringBuilder();
            } else {
                text.delete(0, text.length());
            }
        }

        public void putAttribute(QName qname, String value) {
            attributes.put(qname, value);
        }

        public String getAttribute(QName attributeName) {
            return attributes.get(attributeName);
        }

        public Iterator<Map.Entry<QName, String>> iterator() {
            return new AttributeIterator(attributes.entrySet());
        }
    }

    public static class AttributeIterator implements Iterator<Map.Entry<QName, String>> {
        private Iterator<Map.Entry<QName, String>> iterator;

        public AttributeIterator(Set<Map.Entry<QName, String>> entries) {
            this.iterator = entries.iterator();
        }

        public boolean hasNext() {
            return iterator.hasNext();
        }

        public Map.Entry<QName, String> next() {
            return iterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException("AttributeIterator does not implement method remove().");
        }
    }


//    public boolean findFirstElement() {
//        return findFirstElement((QName) null);
//    }
//
//    public boolean findFirstElement(String nodeName) {
//        return findFirstElement(new QName(nodeName));
//    }

//    public boolean findFirstElement(QName qName) {
//        // handle empty argument
//        if (qName == null || qName.getLocalPart().equals("")) {
//            return nextElementBoundary(false);
//        }
//        while (true) {
//            if (nextElementBoundary(false)) {
//                if (reader.getName().equals(qName)) {
//                    moveDown();
//                    return true;
//                }
//            } else {
//                if (isEnd) {
//                    return false;
//                }
//            }
//        }
//    }

    public QName getRootName() {
        // if root element name is not known yet
        if (rootName == null) {
            boolean bol = nextElementBoundary(false);
            rootName = reader.getName();
            if (isEnd && rootName == null) {
                return null;
            }
        }
        return rootName;
    }

    public void saveSubTree(Object reference) {

        // save only if objectStore is set
        if (objectStore == null) return;

        if (eventCache == null) {
            throw new XmapprException("Error: saved ObjectStore is null.");
        }
        // save a starting position of this block
        objectStore.addStart(reference);

        // calculate current depth of xml nodes in objectStore
        int depth = eventCache.getDepth();

        //copy cached xml events from event cache
        objectStore.copyFrom(eventCache);
        eventCache.reset();

        // check if stream is already at the end of subtree (happens when there is only one empty node in a subtree)
        int event = reader.getEventType();
        if (event == XMLStreamConstants.END_ELEMENT) {
            objectStore.addEnd();
            return;
        } else {
            event = nextEvent();
        }

        boolean loop = true;
        while (loop) {
            switch (event) {
                case XMLStreamConstants.START_ELEMENT:
                    depth++;
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    if (--depth == 0) {
                        loop = false;
                    }
                    break;
            }
            storeElement(objectStore);
            if (loop) {
                event = nextEvent();
            }
        }
        objectStore.addEnd();
    }

    private void storeElement(ObjectStore store) {
        int xmlEventType = reader.getEventType();
        QName qName;
        String name;
        switch (xmlEventType) {
            case XMLStreamConstants.START_DOCUMENT:
                store.addElement(XMLStreamConstants.START_DOCUMENT,
                        (  "UTF-8\n" + settings.version).getBytes());  // save the encoding settings
                break;
            case XMLStreamConstants.END_DOCUMENT:
                store.addElement(XMLStreamConstants.END_DOCUMENT);
                break;
            case XMLStreamConstants.START_ELEMENT:
                qName = reader.getName();
                // save namespace
                store.cacheNamespace(qName.getPrefix(), qName.getNamespaceURI());
                name = qName.getPrefix() + "=" + qName.getLocalPart();
                store.addElement(XMLStreamConstants.START_ELEMENT, name);
                addAtributes(store);
                addNamespaces(store);
                break;
            case XMLStreamConstants.END_ELEMENT:
                store.addElement(XMLStreamConstants.END_ELEMENT);
                break;
            case XMLStreamConstants.CHARACTERS:
                if (!reader.isWhiteSpace()) {
                    store.addElement(XMLStreamConstants.CHARACTERS, reader.getText().trim());
                }
                break;
        }
    }

    private void addAtributes(ObjectStore store) {
        QName qName;
        String name;
        for (int i = 0, n = reader.getAttributeCount(); i < n; ++i) {
            qName = reader.getAttributeName(i);
            name = qName.getPrefix() + "=" + qName.getLocalPart(); //+ qName.getNamespaceURI();
            store.addElement(XMLStreamConstants.ATTRIBUTE, name + "=" + reader.getAttributeValue(i));
        }
    }

    private void addNamespaces(ObjectStore store) {
        String uri;
        String prefix;
        for (int i = 0, n = reader.getNamespaceCount(); i < n; ++i) {
            uri = reader.getNamespaceURI(i);
            prefix = reader.getNamespacePrefix(i);
            store.addElement(XMLStreamConstants.NAMESPACE, prefix + "=" + uri);
        }
    }
}
